{/* Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License. */}

import {Layout} from '@react-spectrum/docs';
export default Layout;

import docs from 'docs:@react-spectrum/table';
import dndDocs from 'docs:@react-spectrum/dnd';
import tableUtil from 'docs:@react-aria/test-utils/src/table.ts';
import tableTypes from 'docs:@react-types/table/src/index.d.ts';
import {HeaderInfo, PropTable, PageDescription, TypeLink, VersionBadge, ClassAPI} from '@react-spectrum/docs';
import {Keyboard} from '@react-spectrum/text';
import packageData from '@react-spectrum/table/package.json';
import ChevronRight from '@spectrum-icons/workflow/ChevronRight';

```jsx import
import {ActionButton} from '@react-spectrum/button';
import Add from '@spectrum-icons/workflow/Add';
import {Cell, Column, Row, TableView, TableBody, TableHeader} from '@react-spectrum/table';
import {Flex} from '@react-spectrum/layout';
```

---
category: Collections
keywords: [table, grid]
---

# TableView

<PageDescription>{docs.exports.TableView.description}</PageDescription>

<HeaderInfo
  packageData={packageData}
  componentNames={['Cell', 'Column', 'Row', 'TableView', 'TableBody', 'TableHeader']}
  sourceData={[
    {type: 'Spectrum', url: 'https://spectrum.adobe.com/page/table/'}
  ]}
  since="3.14.0" />

## Example
```tsx example
<TableView aria-label="Example table with static contents" selectionMode="multiple">
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column align="end">Date Modified</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>Games</Cell>
      <Cell>File folder</Cell>
      <Cell>6/7/2020</Cell>
    </Row>
    <Row>
      <Cell>Program Files</Cell>
      <Cell>File folder</Cell>
      <Cell>4/7/2021</Cell>
    </Row>
    <Row>
      <Cell>bootmgr</Cell>
      <Cell>System file</Cell>
      <Cell>11/20/2010</Cell>
    </Row>
    <Row>
      <Cell>log.txt</Cell>
      <Cell>Text Document</Cell>
      <Cell>1/18/2016</Cell>
    </Row>
  </TableBody>
</TableView>
```

## Content
TableView is a complex [collection component](collections.html) that is built up from many child elements including columns, rows, and cells. Columns are defined within a TableHeader element and rows are defined within a TableBody element. Rows contain Cell elements that correspond to each column. Cells can contain any element, allowing you to have focusable children within the TableView.

Basic usage of TableView, seen in the example above, shows the use of a static collection where the contents of the TableView is hard coded. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API, or update over time. Providing the data in this way allows TableView to automatically cache the rendering of each item, which dramatically improves performance.

Columns and rows can be statically defined as children, or generated dynamically using a function based on the data passed to the `columns` or `items` prop respectively. Cells can also be statically defined as children, or generated dynamically based on the columns defined in the TableHeader.

Each column and row has a unique key defined by the data. In the example below, the `uid` property of the column object is used as the `key` for the Column element within the TableHeader. The `key` of each row element is implicitly defined by the id property of the row object. See [collections](collections.html#unique-keys) to learn more about keys in dynamic collections.

```tsx example
let columns = [
  {name: 'Name', uid: 'name'},
  {name: 'Type', uid: 'type'},
  {name: 'Date Modified', uid: 'date'}
];

let rows = [
  {id: 1, name: 'Games', date: '6/7/2020', type: 'File folder'},
  {id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder'},
  {id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file'},
  {id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document'}
];

<TableView
  aria-label="Example table with dynamic content"
  maxWidth="size-6000">
  <TableHeader columns={columns}>
    {column => (
      <Column
        key={column.uid}
        align={column.uid === 'date' ? 'end' : 'start'}>
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody items={rows}>
    {item => (
      <Row>
        {columnKey => <Cell>{item[columnKey]}</Cell>}
      </Row>
    )}
  </TableBody>
</TableView>
```
### Layout
TableViews are often contained within a page layout that defines the size of the table. For example, a page might have a header or toolbar with a TableView below that fills the remaining vertical space. TableView is designed to scroll internally while the column headers remain fixed. Because of this, TableViews should not be placed within a scrollable container.

The example below shows how to use a [Flex](Flex.html) component to achieve the layout described above. Note the TableView uses the `flex` prop to fill the remainder of the available space.


```tsx example
///- begin collapse -///
let columns = [
  {name: 'First name', id: 'first_name'},
  {name: 'Last name', id: 'last_name'},
  {name: 'City', id: 'city'}
];

let rows = [
{"id":1,"first_name":"Andras","last_name":"Rodmell","city":"Tilburg"},
{"id":2,"first_name":"Hansiain","last_name":"Muino","city":"Hollola"},
{"id":3,"first_name":"Northrop","last_name":"Adnet","city":"Lai Cách"},
{"id":4,"first_name":"Giana","last_name":"Phython","city":"Laspezia"},
{"id":5,"first_name":"Maud","last_name":"Jaram","city":"Tipaz"},
{"id":6,"first_name":"Gasparo","last_name":"Wiggin","city":"Feuknoni"},
{"id":7,"first_name":"Phillie","last_name":"Lezemere","city":"Krajan Sidodadi"},
{"id":8,"first_name":"Kailey","last_name":"Du Plantier","city":"Shangping"},
{"id":9,"first_name":"Brady","last_name":"Oxtarby","city":"Bang Mun Nak"},
{"id":10,"first_name":"Ekaterina","last_name":"Crennan","city":"Santo Antônio do Amparo"},
{"id":11,"first_name":"Jaine","last_name":"Trembey","city":"Manūjān"},
{"id":12,"first_name":"Emmey","last_name":"Dunguy","city":"Garhi Yāsīn"},
{"id":13,"first_name":"Camille","last_name":"Millwall","city":"Orion"},
{"id":14,"first_name":"Staci","last_name":"Glusby","city":"Alofi"},
{"id":15,"first_name":"Ned","last_name":"Crumbleholme","city":"Ban Bueng"},
{"id":16,"first_name":"Tana","last_name":"Beardsworth","city":"Puerto Aisén"},
{"id":17,"first_name":"Dewain","last_name":"Fladgate","city":"London"},
{"id":18,"first_name":"Thurstan","last_name":"Trembath","city":"Orléans"},
{"id":19,"first_name":"Vaclav","last_name":"Fitzpayn","city":"Huangchen"},
{"id":20,"first_name":"Keven","last_name":"Monkeman","city":"Medenychi"},
{"id":21,"first_name":"Talia","last_name":"Ryman","city":"Piteå"},
{"id":22,"first_name":"Percy","last_name":"Le Teve","city":"Terny"},
{"id":23,"first_name":"Jackson","last_name":"Anten","city":"Beiling"},
{"id":24,"first_name":"Jakob","last_name":"Goullee","city":"Pelym"},
{"id":25,"first_name":"Dru","last_name":"Klainer","city":"Zavrč"},
{"id":26,"first_name":"Lucie","last_name":"Donahue","city":"Kiryū"},
{"id":27,"first_name":"Marc","last_name":"McPeck","city":"Nong Muang Khai"},
{"id":28,"first_name":"Vivianna","last_name":"Allport","city":"Kajatian"},
{"id":29,"first_name":"Drud","last_name":"Hurn","city":"Bambuí"},
{"id":30,"first_name":"Trever","last_name":"Ambrodi","city":"Xiangtan"},
{"id":31,"first_name":"Gwennie","last_name":"Kingswold","city":"San Benito"},
{"id":32,"first_name":"Karlan","last_name":"Tilby","city":"Patrída"},
{"id":33,"first_name":"Heddie","last_name":"Sneath","city":"Esperanza"},
{"id":34,"first_name":"Harlen","last_name":"Sandells","city":"Harrismith"},
{"id":35,"first_name":"Gavan","last_name":"Halward","city":"Al Ḩayfah"},
{"id":36,"first_name":"Andre","last_name":"Everest","city":"Bahui"},
{"id":37,"first_name":"Merilyn","last_name":"Rowbrey","city":"Imishli"},
{"id":38,"first_name":"Abe","last_name":"Pecht","city":"Pangkalan Kasai"},
{"id":39,"first_name":"Britt","last_name":"Collingridge","city":"Érd"},
{"id":40,"first_name":"Leticia","last_name":"Thorndycraft","city":"Paita"},
{"id":41,"first_name":"Eward","last_name":"Tigwell","city":"Aral"},
{"id":42,"first_name":"Torrie","last_name":"Curzon","city":"Stockholm"},
{"id":43,"first_name":"Jenifer","last_name":"Swalwel","city":"Jinniu"},
{"id":44,"first_name":"Marianna","last_name":"Radley","city":"Hedi"},
{"id":45,"first_name":"Antoine","last_name":"Tyers","city":"Hewa"},
{"id":46,"first_name":"Darline","last_name":"Gallehawk","city":"København"},
{"id":47,"first_name":"Rikki","last_name":"Rosenzveig","city":"Affery"},
{"id":48,"first_name":"Debera","last_name":"Vedenichev","city":"Żywiec"},
{"id":49,"first_name":"Morena","last_name":"Hewins","city":"Las Lajas"},
{"id":50,"first_name":"Cordy","last_name":"Reimer","city":"Derbent"},
{"id":51,"first_name":"Quint","last_name":"Thoresbie","city":"Guyang"},
{"id":52,"first_name":"Christean","last_name":"Deere","city":"Waegwan"},
{"id":53,"first_name":"Moyra","last_name":"Battelle","city":"Villa Presidente Frei, Ñuñoa, Santiago, Chile"},
{"id":54,"first_name":"Fayth","last_name":"Gallafant","city":"Kedungharjo"},
{"id":55,"first_name":"Thedrick","last_name":"Duddy","city":"Thị Trấn Mường Lát"},
{"id":56,"first_name":"George","last_name":"Rickerd","city":"Zarqa"},
{"id":57,"first_name":"Nikos","last_name":"Rideout","city":"Yuanqiao"},
{"id":58,"first_name":"Alejandra","last_name":"Le Port","city":"Il’ichëvo"},
{"id":59,"first_name":"Eleonora","last_name":"Gibberd","city":"Sua"},
{"id":60,"first_name":"Archibaldo","last_name":"Place","city":"Sidayu"},
{"id":61,"first_name":"Helen","last_name":"Brenton","city":"Kuressaare"},
{"id":62,"first_name":"Leyla","last_name":"Armstead","city":"Haifa"},
{"id":63,"first_name":"Bridget","last_name":"Strotone","city":"Karasuk"},
{"id":64,"first_name":"Jarid","last_name":"Packer","city":"Студеничани"},
{"id":65,"first_name":"Christos","last_name":"Natt","city":"Nova Russas"},
{"id":66,"first_name":"Alwyn","last_name":"Mingaud","city":"Conde"},
{"id":67,"first_name":"Archy","last_name":"Thorneywork","city":"Gulu"},
{"id":68,"first_name":"Iolanthe","last_name":"Spurgeon","city":"Ayrihuanca"},
{"id":69,"first_name":"Rossy","last_name":"Axford","city":"Ledeč nad Sázavou"},
{"id":70,"first_name":"Consuela","last_name":"Lillegard","city":"Finote Selam"},
{"id":71,"first_name":"Salomon","last_name":"Buckney","city":"Kampokpok"},
{"id":72,"first_name":"Celene","last_name":"Espley","city":"Sinubong"},
{"id":73,"first_name":"Kristos","last_name":"Denyukhin","city":"Las Palmas"},
{"id":74,"first_name":"Bertha","last_name":"Mallabon","city":"Vera"},
{"id":75,"first_name":"Jorry","last_name":"Yuryev","city":"Carletonville"},
{"id":76,"first_name":"Holly-anne","last_name":"Wagstaffe","city":"Sukadana"},
{"id":77,"first_name":"Lara","last_name":"Shears","city":"Gambēla"},
{"id":78,"first_name":"Romonda","last_name":"Glanville","city":"Donglu"},
{"id":79,"first_name":"Felice","last_name":"Pryde","city":"Sapadun"},
{"id":80,"first_name":"Nick","last_name":"Kidney","city":"Chernigovka"},
{"id":81,"first_name":"Hermina","last_name":"Dooley","city":"New Agutaya"},
{"id":82,"first_name":"Ketty","last_name":"FitzGeorge","city":"Abaza"},
{"id":83,"first_name":"Patrizio","last_name":"Bovingdon","city":"‘Ayn al ‘Arab"},
{"id":84,"first_name":"Caitrin","last_name":"Braine","city":"Il’inskiy"},
{"id":85,"first_name":"Ian","last_name":"De Few","city":"Jatinagara"},
{"id":86,"first_name":"Eben","last_name":"Adan","city":"Bolong"},
{"id":87,"first_name":"Peder","last_name":"Innott","city":"Gampaha"},
{"id":88,"first_name":"Selie","last_name":"Cruise","city":"Mariscala"},
{"id":89,"first_name":"Melania","last_name":"Meredyth","city":"La’ershan"},
{"id":90,"first_name":"Antonina","last_name":"Proby","city":"Shantoudian"},
{"id":91,"first_name":"Sabra","last_name":"Dreng","city":"Dzhankoy"},
{"id":92,"first_name":"Sibeal","last_name":"Hall-Gough","city":"Mengxi"},
{"id":93,"first_name":"Fidel","last_name":"Maisey","city":"Gus’-Khrustal’nyy"},
{"id":94,"first_name":"Alejandro","last_name":"Devey","city":"Charata"},
{"id":95,"first_name":"Norina","last_name":"Stoyle","city":"Malaya Dubna"},
{"id":96,"first_name":"Lari","last_name":"Kiezler","city":"Guaíba"},
{"id":97,"first_name":"Percival","last_name":"Geffinger","city":"Ngeni"},
{"id":98,"first_name":"Jo","last_name":"Spoure","city":"Karata"},
{"id":99,"first_name":"Karlie","last_name":"Gooddy","city":"Pelem"},
{"id":100,"first_name":"Edmon","last_name":"Alsopp","city":"Sandu"}];
///- end collapse -///

<Flex height="size-5000" width="100%" direction="column" gap="size-150">
  <ActionButton alignSelf="start">Add</ActionButton>
  <TableView
    flex
    aria-label="Example table with dynamic content">
    <TableHeader columns={columns}>
      {column => (
        <Column
          key={column.id}>
          {column.name}
        </Column>
      )}
    </TableHeader>
    <TableBody items={rows}>
      {item => (
        <Row>
          {columnKey => <Cell>{item[columnKey]}</Cell>}
        </Row>
      )}
    </TableBody>
  </TableView>
</Flex>
```

### Internationalization
To internationalize a TableView, all text content within the TableView should be localized. This includes the `aria-label` provided to the TableView if any.
For languages that are read right-to-left (e.g. Hebrew and Arabic), the layout of TableView is automatically flipped.

## Labeling
### Accessibility
An `aria-label` must be provided to the TableView for accessibility. If the TableView is labeled by a separate element, an `aria-labelledby` prop must be provided using the id of the labeling element instead.

By default, the first column of the TableView is used as the [row header](https://www.w3.org/TR/wai-aria-1.1/#rowheader) and is announced by assistive technology when navigating through the rows. You can override this behavior by providing the `isRowHeader` prop
to one or more Columns, allowing you to customize which columns should label the rows of the TableView.

The example below applies `isRowHeader` to the "First Name" and "Last Name" columns so that each row is announced with the person's full name (e.g. "John Doe").

```tsx example
<TableView aria-label="Example table with static contents">
  <TableHeader>
    <Column isRowHeader>First Name</Column>
    <Column isRowHeader>Last Name</Column>
    <Column align="end">Age</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>John</Cell>
      <Cell>Doe</Cell>
      <Cell>45</Cell>
    </Row>
    <Row>
      <Cell>Jane</Cell>
      <Cell>Doe</Cell>
      <Cell>37</Cell>
    </Row>
    <Row>
      <Cell>Joe</Cell>
      <Cell>Schmoe</Cell>
      <Cell>67</Cell>
    </Row>
  </TableBody>
</TableView>
```

## Asynchronous loading
TableView supports loading data asynchronously, and will display a progress circle reflecting the current load state,
set by the `loadingState` prop. It also supports infinite scrolling to load more data on demand as the user scrolls, via the `onLoadMore` prop.

This example uses the [useAsyncList](react-aria:useAsyncList.html) hook to handle loading the data.
See the docs for more information.

```tsx example
import {useAsyncList} from '@react-stately/data';

interface Character {
  name: string,
  height: number,
  mass: number,
  birth_year: number
}

function AsyncTable() {
  let columns = [
    {name: 'Name', key: 'name'},
    {name: 'Height', key: 'height'},
    {name: 'Mass', key: 'mass'},
    {name: 'Birth Year', key: 'birth_year'}
  ];

  let list = useAsyncList<Character>({
    async load({signal, cursor}) {
      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      let res = await fetch(cursor || `https://swapi.py4e.com/api/people/?search=`, {signal});
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });

  return (
    <TableView aria-label="example async loading table" height="size-3000">
      <TableHeader columns={columns}>
        {(column) => (
          <Column align={column.key !== 'name' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody
        items={list.items}
        loadingState={list.loadingState}
        onLoadMore={list.loadMore}>
        {(item) => (
          <Row key={item.name}>{(key) => <Cell>{item[key]}</Cell>}</Row>
        )}
      </TableBody>
    </TableView>
  );
}
```


## Selection

By default, TableView doesn't allow row selection but this can be enabled using the `selectionMode` prop.
Use `defaultSelectedKeys` to provide a default set of selected rows. Note that the value of the selected keys must match the `key` prop of the Row.

The example below enables multiple selection mode, and uses `defaultSelectedKeys` to select the rows with keys "2" and "4".

```tsx example
<TableView aria-label="Example table with multiple selection" selectionMode="multiple" defaultSelectedKeys={['2', '4']}>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column align="end">Level</Column>
  </TableHeader>
  <TableBody>
    <Row key="1">
      <Cell>Charizard</Cell>
      <Cell>Fire, Flying</Cell>
      <Cell>67</Cell>
    </Row>
    <Row key="2">
      <Cell>Blastoise</Cell>
      <Cell>Water</Cell>
      <Cell>56</Cell>
    </Row>
    <Row key="3">
      <Cell>Venusaur</Cell>
      <Cell>Grass, Poison</Cell>
      <Cell>83</Cell>
    </Row>
    <Row key="4">
      <Cell>Pikachu</Cell>
      <Cell>Electric</Cell>
      <Cell>100</Cell>
    </Row>
  </TableBody>
</TableView>
```

### Controlled selection

To programmatically control row selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `key` prop from the selected rows will
be passed into the callback when the row is pressed, allowing you to update state accordingly.

Here is how you would control selection for the above example.

```tsx example export=true
import type {Selection} from '@adobe/react-spectrum';

function PokemonTable(props) {
  let columns = [
    {name: 'Name', uid: 'name'},
    {name: 'Type', uid: 'type'},
    {name: 'Level', uid: 'level'}
  ];

  let rows = [
    {id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67'},
    {id: 2, name: 'Blastoise', type: 'Water', level: '56'},
    {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83'},
    {id: 4, name: 'Pikachu', type: 'Electric', level: '100'}
  ];

  let [selectedKeys, setSelectedKeys] = React.useState<Selection>(new Set([2]));

  return (
    <TableView aria-label="Table with controlled selection" selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props}>
      <TableHeader columns={columns}>
        {column => (
          <Column key={column.uid} align={column.uid === 'level' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={rows}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
```

### Single selection

To limit users to selecting only a single item at a time, `selectionMode` can be set to `single`.

```tsx example
// Using the same table as above
<PokemonTable selectionMode="single" />
```

### Disallow empty selection

TableView also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the TableView selected at all times.
In this mode, if a single row is selected and the user presses it, it will not be deselected.

```tsx example
// Using the same table as above
<PokemonTable disallowEmptySelection />
```

### Disabled rows

You can disable specific rows by providing an array of keys to TableView via the `disabledKeys` prop. This will prevent rows from being selectable as shown in the example below.

```tsx example
// Using the same table as above
<PokemonTable selectionMode="multiple" disabledKeys={[3]} />
```

### Highlight selection

By default, TableView uses the checkbox selection style, which includes a checkbox in each row for selection. When the selectionStyle prop is set to `"highlight"`, the checkboxes are hidden,
and the selected rows are displayed with a highlighted background instead.

In addition to changing the appearance, the selection behavior also changes depending on the `selectionStyle` prop. In the default checkbox selection style, clicking, tapping, or pressing
the <Keyboard>Space</Keyboard> or <Keyboard>Enter</Keyboard> keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection.

In the highlight selection style, however, clicking a row with the mouse _replaces_ the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows,
modifier keys such as <Keyboard>Ctrl</Keyboard>, <Keyboard>Cmd</Keyboard>, and <Keyboard>Shift</Keyboard> can be used. On touch screen devices, selection always behaves as toggle since modifier
keys may not be available.

These selection styles implement the behaviors defined in [Aria Practices](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/#keyboardinteraction).

```tsx example
<PokemonTable selectionMode="multiple" selectionStyle="highlight" />
```

## Row actions

TableView supports row actions via the `onAction` prop, which is useful for functionality such as navigation. When nothing is selected, the TableView performs actions by default when clicking or tapping a row.
Items may be selected using the checkbox, or by long pressing on touch devices. When at least one item is selected, the TableView is in selection mode,
and clicking or tapping a row toggles the selection. Actions may also be triggered via the <Keyboard>Enter</Keyboard> key, and selection using the <Keyboard>Space</Keyboard> key.

This behavior is slightly different in the highlight selection style, where single clicking selects the row and actions are performed via double click. Touch and keyboard behaviors are unaffected.

```tsx example
<Flex direction="column" gap="size-300">
  <PokemonTable aria-label="Pokemon table with row actions and checkbox selection" selectionMode="multiple" onAction={key => alert(`Opening item ${key}...`)} />
  <PokemonTable aria-label="Pokemon table with row actions and highlight selection" selectionMode="multiple" selectionStyle="highlight" onAction={key => alert(`Opening item ${key}...`)} />
</Flex>
```

### Links

Table rows may also be links to another page or website. This can be achieved by passing the `href` prop to the `<Row>` component. Links behave the same way as described above for row actions depending on the `selectionMode` and `selectionStyle`.

```tsx example
<TableView aria-label="Bookmarks" selectionMode="multiple">
  <TableHeader>
    <Column>Name</Column>
    <Column>URL</Column>
    <Column>Date added</Column>
  </TableHeader>
  <TableBody>
    <Row href="https://adobe.com/" target="_blank">
      <Cell>Adobe</Cell>
      <Cell>https://adobe.com/</Cell>
      <Cell>January 28, 2023</Cell>
    </Row>
    <Row href="https://google.com/" target="_blank">
      <Cell>Google</Cell>
      <Cell>https://google.com/</Cell>
      <Cell>April 5, 2023</Cell>
    </Row>
    <Row href="https://nytimes.com/" target="_blank">
      <Cell>New York Times</Cell>
      <Cell>https://nytimes.com/</Cell>
      <Cell>July 12, 2023</Cell>
    </Row>
  </TableBody>
</TableView>
```

#### Client side routing

The `<Row>` component works with frameworks and client side routers like [Next.js](https://nextjs.org/) and [React Router](https://reactrouter.com/en/main). As with other React Spectrum components that support links, this works via the [Provider](Provider.html) component at the root of your app. See the [client side routing guide](routing.html) to learn how to set this up.

## Sorting

TableView supports sorting its data when a column header is pressed. To designate that a Column should support sorting, provide it with
the `allowsSorting` prop. The TableView accepts a `sortDescriptor` prop that defines the current column key to sort by and the sort direction (ascending/descending).
When the user presses a sortable column header, the column's key and sort direction is passed into the `onSortChange` callback, allowing you to update the `sortDescriptor` appropriately.

This example performs client side sorting by passing a `sort` function to the [useAsyncList](react-aria:useAsyncList.html) hook.
See the docs for more information on how to perform server side sorting.

```tsx example
import {useCollator} from '@adobe/react-spectrum';

interface Character {
  name: string,
  height: number,
  mass: number,
  birth_year: number
}

function AsyncSortTable() {
  let collator = useCollator({numeric: true});

  let list = useAsyncList<Character>({
    async load({signal}) {
      let res = await fetch(`https://swapi.py4e.com/api/people/?search`, {signal});
      let json = await res.json();
      return {
        items: json.results
      };
    },
    async sort({items, sortDescriptor}) {
      return {
        items: items.sort((a, b) => {
          let first = a[sortDescriptor.column];
          let second = b[sortDescriptor.column];
          let cmp = collator.compare(first, second);
          if (sortDescriptor.direction === 'descending') {
            cmp *= -1;
          }
          return cmp;
        })
      };
    }
  });

  return (
    <TableView aria-label="Example table with client side sorting" sortDescriptor={list.sortDescriptor} onSortChange={list.sort} height="size-3000">
      <TableHeader>
        <Column key="name" allowsSorting>Name</Column>
        <Column key="height" allowsSorting>Height</Column>
        <Column key="mass" allowsSorting>Mass</Column>
        <Column key="birth_year" allowsSorting>Birth Year</Column>
      </TableHeader>
      <TableBody
        items={list.items}
        loadingState={list.loadingState}>
        {item => (
          <Row key={item.name}>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
```

## Column widths

By default, TableView divides the available space evenly among the columns. The `Column` component also supports four different width props that allow you to control column sizing behavior: `defaultWidth`, `width`, `minWidth`, and `maxWidth`.

The `width` and `defaultWidth` props define the width of a column. The former defines a controlled width, and the latter defines an uncontrolled width when the column is [resizable](#column-resizing). These props accept fixed pixel values, percentages of the total table width, or [fractional](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout#the_fr_unit) values (the `fr` unit), which represent a fraction of the available space. Columns without a defined width are equivalent to `1fr`.

The `minWidth` and `maxWidth` props define constraints on the size of a column, which may be defined either as fixed pixel values or as percentages of the total table width. These are respected when calculating the size of a column, and also provide limits for column resizing.

```tsx example
<TableView aria-label="Example table for column widths" maxWidth={320}>
  <TableHeader>
    <Column defaultWidth="1fr" align="start">Name</Column>
    <Column maxWidth={80}>Type</Column>
    <Column width={80}>Size</Column>
    <Column minWidth={100} align="end">Date Modified</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
      <Cell>April 12</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
      <Cell>November 27</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
      <Cell>January 7</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
      <Cell>February 11</Cell>
    </Row>
  </TableBody>
</TableView>
```

## Column Resizing

TableView supports resizable columns, allowing users to dynamically adjust the width of a column. To designate that a Column is resizable, provide it with the `allowsResizing` prop. This will render a draggable
resizer handle that becomes visible on hover. Keyboard, touch, and screen reader users can start resizing by interacting with the target column's header and selecting the "Resize column" option
from the dropdown.

### Width values

An initial, uncontrolled width can be provided to a Column using the `defaultWidth` prop. This allows the column width to freely shrink and grow in relation to other column widths. Alternatively, a controlled value can be provided by the `width` prop. The `minWidth` and `maxWidth` props allow you to restrict a Column's size. See [column widths](#column-widths) above for more details.

The example below illustrates how each of the column width props affects their respective column's resize behavior.

```tsx example
<TableView
  aria-label="TableView with resizable columns"
  maxWidth={320}
  height={210} >
  <TableHeader>
    {/*- begin highlight -*/}
    <Column key="file" allowsResizing maxWidth={500}>File Name</Column>
    <Column key="size" width={80}>Size</Column>
    <Column key="date" allowsResizing minWidth={100}>Date Modified</Column>
    {/*- end highlight -*/}
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2022-Roadmap-Proposal-Revision-012822-Copy(2)</Cell>
      <Cell>214 KB</Cell>
      <Cell>November 27, 2022 at 4:56PM</Cell>
    </Row>
    <Row>
      <Cell>62259692_p0_master1200</Cell>
      <Cell>120 KB</Cell>
      <Cell>January 27, 2021 at 1:56AM</Cell>
    </Row>
    <Row>
      <Cell colSpan={3}>Total space: 334 KB</Cell>
    </Row>
  </TableBody>
</TableView>
```

### Resize events

TableView accepts an `onResize` prop which is triggered whenever a column resizer is moved by the user. This can be used in combination with the `width` prop to update a Column's width in a controlled fashion. TableView also accepts
an `onResizeEnd` prop, which is triggered when the user finishes a column resize operation. Both events receive a Map object containing the widths of every column in the TableView.

The example below uses `onResize` to update each of the TableView's controlled column widths. It also saves the finalized column widths to `localStorage` in `onResizeEnd`, allowing the TableView's state to be preserved between
page loads and refreshes.

```tsx example
let items = [
  {id: '1', file: '2022-Roadmap-Proposal-Revision-012822-Copy(2)', size: '214 KB', date: 'November 27, 2022 at 4:56PM'},
  {id: '2', file: '62259692_p0_master1200', size: '120 KB', date: 'January 27, 2021 at 1:56AM'}
];

let columnsData = [
  {name: 'File Name', id: 'file', width: '1fr'},
  {name: 'Size', id: 'size', width: 80},
  {name: 'Date', id: 'date', width: 100}
];

function ResizableTable() {
  /*- begin highlight -*/
  let [columns, setColumns] = React.useState(() => {
    let localStorageWidths = localStorage.getItem('RSPWidths');
    if (localStorageWidths) {
      let widths = JSON.parse(localStorageWidths);
      return columnsData.map(col => ({...col, width: widths[col.id]}));
    } else {
      return columnsData;
    }
  });

  let onResize = (widths) => {
    setColumns(columns => columns.map(col => ({...col, width: widths.get(col.id)})));
  };

  let onResizeEnd = (widths) => {
    localStorage.setItem('RSPWidths', JSON.stringify(Object.fromEntries(widths)));
  };
  /*- end highlight -*/

  return (
    <TableView
      /*- begin highlight -*/
      onResize={onResize}
      onResizeEnd={onResizeEnd}
      /*- end highlight -*/
      aria-label="TableView with controlled, resizable columns saved in local storage"
      maxWidth={320}
      height={200} >
      <TableHeader columns={columns}>
        {(column) => {
          const {name, id, width} = column;
          return <Column allowsResizing key={id} width={width}>{name}</Column>;
        }}
      </TableHeader>
      <TableBody items={items}>
        {(item) => (
          <Row key={item.id}>{(key) => <Cell>{item[key]}</Cell>}</Row>
        )}
      </TableBody>
    </TableView>
  );
}

<ResizableTable />
```


## Drag and drop <VersionBadge version="beta" style={{marginLeft: 4, verticalAlign: 'bottom'}} />

To enable drag and drop in a TableView, you must provide the drag and drop hooks sourced from <TypeLink links={dndDocs.links} type={dndDocs.exports.useDragAndDrop} /> to the TableView's `dragAndDropHooks` prop.
See the examples below for various common drag and drop use cases. For more information on `useDragAndDrop` and the various supported ways to perform a drag and drop interaction,
please see the [drag and drop documentation](dnd.html).

### Draggable TableView and droppable TableView

The example below demonstrates how to create a draggable TableView and a droppable TableView.

<details>
  <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>

```tsx example export=true render=false
import type {DragAndDropOptions, TextDropItem} from '@react-spectrum/dnd';
import type {ListData} from '@adobe/react-spectrum';
import {useDragAndDrop} from '@react-spectrum/dnd';
import {useListData} from '@adobe/react-spectrum';

interface Item {
  name: string,
  type?: string,
  date?: string,
  childNodes?: Item[]
}

interface DndTableViewProps extends DragAndDropOptions {
  list: ListData<Item>,
  columns: { name: string; id: string; }[]
}

function DraggableTableView(props: DndTableViewProps) {
  let {list, columns, ...otherProps} = props;
  /*- begin highlight -*/
  let {dragAndDropHooks} = useDragAndDrop({
    // Only allow move operations when dropping items from this table
    getAllowedDropOperations: () => ['move'],
    getItems: (keys) => [...keys].map(key => {
      let item = list.getItem(key);
      // Setup the drag types and associated info for each dragged item.
      return {
        'custom-app-type': JSON.stringify(item),
        'text/plain': item.name
      };
    }),
    onDragEnd: (e) => {
      let {
        dropOperation,
        keys
      } = e;

      if (dropOperation === 'move') {
        list.remove(...keys);
      }
    },
    ...otherProps
  });
  /*- end highlight -*/

  return (
    <TableView
      aria-label="Draggable TableView in drag into table example"
      selectionMode="multiple"
      width="size-5000"
      height="size-2400"
      /*- begin highlight -*/
      dragAndDropHooks={dragAndDropHooks}
      /*- end highlight -*/>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DroppableTableView(props: DndTableViewProps) {
  let {list, columns, ...otherProps} = props;
  /*- begin highlight -*/
  let {dragAndDropHooks} = useDragAndDrop({
    // Only accept items with the following drag type
    acceptedDragTypes: ['custom-app-type'],
    onInsert: async (e) => {
      let {
        items,
        target
      } = e;

      let processedItems = await Promise.all(
        items.map(async (item) => JSON.parse(await (item as TextDropItem).getText('custom-app-type')))
      );

      if (target.dropPosition === 'before') {
        list.insertBefore(target.key, ...processedItems);
      } else if (target.dropPosition === 'after') {
        list.insertAfter(target.key, ...processedItems);
      }
    },
    onRootDrop: async (e) => {
      let {
        items
      } = e;
      let processedItems = await Promise.all(
        items.map(async (item) => JSON.parse(await (item as TextDropItem).getText('custom-app-type')))
      );
      list.append(...processedItems);
    },
    ...otherProps
  });
  /*- end highlight -*/

  return (
    <TableView
      aria-label="Droppable TableView in drag into table example"
      width="size-5000"
      height="size-2400"
      /*- begin highlight -*/
      dragAndDropHooks={dragAndDropHooks}
      /*- end highlight -*/>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DragIntoTable() {
  let columns = [
    {name: 'Name', id: 'name'},
    {name: 'Type', id: 'type'},
    {name: 'Date Modified', id: 'date'}
  ];

  let sourceList = useListData({
    initialItems: [
      {id: '1', type: 'file', name: 'Adobe Photoshop', date: '6/7/2020'},
      {id: '2', type: 'file', name: 'Adobe XD', date: '4/7/2021'},
      {id: '3', type: 'file', name: 'Adobe InDesign', date: '11/20/2010'},
      {id: '4', type: 'file', name: 'Adobe AfterEffects', date: '1/18/2016'}
    ]
  });

  let targetList = useListData({
    initialItems: [
      {id: '5', type: 'file', name: 'Adobe Dreamweaver', date: '1/18/2016'},
      {id: '6', type: 'file', name: 'Adobe Fresco', date: '11/20/2010'},
      {id: '7', type: 'file', name: 'Adobe Connect', date: '4/7/2021'},
      {id: '8', type: 'file', name: 'Adobe Lightroom', date: '6/7/2020'}
    ]
  });

  return (
    <Flex wrap gap="size-300">
      <DraggableTableView list={sourceList} columns={columns} />
      <DroppableTableView list={targetList} columns={columns} />
    </Flex>
  );
}
```
</details>

```tsx example
<DragIntoTable />
```

### Handling folder drops

The example below replicates the previous example but demonstrates how to handle on item drops.

<details>
  <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>

```tsx example export=true render=false
function DraggableTableViewFolder(props: DndTableViewProps) {
  let {list, columns, ...otherProps} = props;
  let {dragAndDropHooks} = useDragAndDrop({
    // Only allow move operations when dropping items from this table
    getAllowedDropOperations: () => ['move'],
    getItems: (keys) => [...keys].map(key => {
      let item = list.getItem(key);
      // Setup the drag types and associated info for each dragged item.
      return {
        'custom-app-type-folder-drop': JSON.stringify(item),
        'text/plain': item.name
      };
    }),
    onDragEnd: (e) => {
      let {
        dropOperation,
        keys
      } = e;

      if (dropOperation === 'move') {
        list.remove(...keys);
      }
    },

    ...otherProps
  });

  return (
    <TableView
      aria-label="Draggable TableView in drag onto folder example"
      selectionMode="multiple"
      width="size-5000"
      height="size-3600"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DroppableTableViewFolder(props: DndTableViewProps) {
  let {list, columns, ...otherProps} = props;
  let {dragAndDropHooks} = useDragAndDrop({
    /*- begin highlight -*/
    // Only allow drops on items with childNodes aka folders
    shouldAcceptItemDrop: (target) => !!list.getItem(target.key).childNodes,
    /*- end highlight -*/
    // Only accept items with the following drag type
    acceptedDragTypes: ['custom-app-type-folder-drop'],
    onInsert: async (e) => {
      let {
        items,
        target
      } = e;

      let processedItems = await Promise.all(
        items.map(async (item) => JSON.parse(await (item as TextDropItem).getText('custom-app-type-folder-drop')))
      );

      if (target.dropPosition === 'before') {
        list.insertBefore(target.key, ...processedItems);
      } else if (target.dropPosition === 'after') {
        list.insertAfter(target.key, ...processedItems);
      }
    },
    onRootDrop: async (e) => {
      let {
        items
      } = e;
      let processedItems = await Promise.all(
        items.map(async (item) => JSON.parse(await (item as TextDropItem).getText('custom-app-type-folder-drop')))
      );
      list.append(...processedItems);
    },
    /*- begin highlight -*/
    onItemDrop: async (e) => {
      let {
        items,
        target
      } = e;

      let processedItems = await Promise.all(
        items.map(async (item) => JSON.parse(await (item as TextDropItem).getText('custom-app-type-folder-drop')))
      );

      let targetItem = list.getItem(target.key);
      list.update(target.key, {...targetItem, childNodes: [...targetItem.childNodes, ...processedItems]});
    },
    /*- end highlight -*/
    ...otherProps
  });

  return (
    <TableView
      aria-label="Droppable TableView in drop into folder example"
      width="size-5000"
      height="size-3600"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => (
              <Cell>
                {item[columnKey]}
                {columnKey === 'type' && item[columnKey] === 'folder' && item.childNodes.length > 0 &&
                 ` (${item.childNodes?.length} dropped item(s)`
                }
              </Cell>
            )}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DragIntoTableFolder() {
  let columns = [
    {name: 'Name', id: 'name'},
    {name: 'Type', id: 'type'},
    {name: 'Date Modified', id: 'date'}
  ];

  let sourceList = useListData({
    initialItems: [
      {id: '1', type: 'file', name: 'Adobe Photoshop', date: '2/1/2020'},
      {id: '2', type: 'file', name: 'Adobe XD', date: '6/7/2020'},
      {id: '3', type: 'folder', name: 'Documents', date: '6/13/2020', childNodes: []},
      {id: '4', type: 'file', name: 'Adobe InDesign', date: '2/4/2020'},
      {id: '5', type: 'folder', name: 'Utilities', date: '12/3/2020', childNodes: []},
      {id: '6', type: 'file', name: 'Adobe AfterEffects', date: '3/12/2020'}
    ]
  });

  let targetList = useListData({
    initialItems: [
      {id: '7', type: 'folder', name: 'Pictures', date: '6/17/2020', childNodes: []},
      {id: '8', type: 'file', name: 'Adobe Fresco', date: '1/27/2020'},
      {id: '9', type: 'folder', name: 'Apps', date: '12/23/2020', childNodes: []}
    ]
  });

  return (
    <Flex wrap gap="size-300">
      <DraggableTableViewFolder list={sourceList} columns={columns} />
      <DroppableTableViewFolder list={targetList} columns={columns} />
    </Flex>
  );
}
```
</details>

```tsx example
<DragIntoTableFolder />
```

### Reorderable

The example below demonstrates how to make a TableView draggable and droppable at the same time. The TableView below supports reordering its own rows via drag and drop.
<details>
  <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>

```tsx example export=true render=false
function ReorderableTable() {
  let columns = [
    {name: 'Name', id: 'name'},
    {name: 'Type', id: 'type'},
    {name: 'Date Modified', id: 'date'}
  ];

  let list = useListData({
    initialItems: [
      {id: '1', type: 'file', name: 'Adobe Photoshop', date: '6/3/2020'},
      {id: '2', type: 'file', name: 'Adobe XD', date: '2/2/2020'},
      {id: '3', type: 'folder', name: 'Documents', date: '5/1/2020', childNodes: []},
      {id: '4', type: 'file', name: 'Adobe InDesign', date: '5/9/2020'},
      {id: '5', type: 'folder', name: 'Utilities', date: '12/7/2020', childNodes: []},
      {id: '6', type: 'file', name: 'Adobe AfterEffects', date: '6/1/2020'}
    ]
  });

  let {dragAndDropHooks} = useDragAndDrop({
    getItems(keys) {
      return [...keys].map(key => {
        let item = list.getItem(key);
        // Setup the drag types and associated info for each dragged item.
        return {
          'custom-app-type-reorder': JSON.stringify(item),
          'text/plain': item.name
        };
      });
    },
    acceptedDragTypes: ['custom-app-type-reorder'],
    /*- begin highlight -*/
    onReorder: async (e) => {
      let {keys, target} = e;

      if (target.dropPosition === 'before') {
        list.moveBefore(target.key, [...keys]);
      } else if (target.dropPosition === 'after') {
        list.moveAfter(target.key, [...keys]);
      }
    },
    getAllowedDropOperations: () => ['move']
    /*- end highlight -*/
  });

  return (
    <TableView
      aria-label="Reorderable TableView"
      selectionMode="multiple"
      height="size-3600"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
```
</details>

```tsx example
<ReorderableTable />
```

### Bi-directional dragging

The example below demonstrates how to create a pair of TableViews that supports dragging and dropping any items between each table, but disables
the ability to drop into a folder. Each table is internally reorderable.
<details>
  <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>

```tsx example export=true render=false
function BidirectionalDnDTableView(props: DndTableViewProps) {
  let {list, columns} = props;
  let {dragAndDropHooks} = useDragAndDrop({
    acceptedDragTypes: ['custom-app-type-bidirectional'],
    // Only allow move operations
    getAllowedDropOperations: () => ['move'],
    getItems(keys) {
      return [...keys].map(key => {
        let item = list.getItem(key);
        // Setup the drag types and associated info for each dragged item.
        return {
          'custom-app-type-bidirectional': JSON.stringify(item),
          'text/plain': item.name
        };
      });
    },
    onInsert: async (e) => {
      let {
        items,
        target
      } = e;
      let processedItems = await Promise.all(
        items.map(async (item) => JSON.parse(await (item as TextDropItem).getText('custom-app-type-bidirectional')))
      );
      if (target.dropPosition === 'before') {
        list.insertBefore(target.key, ...processedItems);
      } else if (target.dropPosition === 'after') {
        list.insertAfter(target.key, ...processedItems);
      }
    },
    onReorder: async (e) => {
      let {
        keys,
        target
      } = e;

      if (target.dropPosition === 'before') {
        list.moveBefore(target.key, [...keys]);
      } else if (target.dropPosition === 'after') {
        list.moveAfter(target.key, [...keys]);
      }
    },
    onRootDrop: async (e) => {
      let {
        items
      } = e;
      let processedItems = await Promise.all(
        items.map(async item => JSON.parse(await (item as TextDropItem).getText('custom-app-type-bidirectional')))
      );
      list.append(...processedItems);
    },
    /*- begin highlight -*/
    onDragEnd: (e) => {
      let {
        dropOperation,
        keys,
        isInternal
      } = e;
      // Only remove the dragged items if they aren't dropped inside the source list
      if (dropOperation === 'move' && !isInternal) {
        list.remove(...keys);
      }
    }
    /*- end highlight -*/
  });

  return (
    <TableView
      aria-label={props['aria-label']}
      selectionMode="multiple"
      width="size-5000"
      height="size-3600"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DragBetweenTablesExample() {
  let columns = [
    {name: 'Name', id: 'name'},
    {name: 'Type', id: 'type'},
    {name: 'Date Modified', id: 'date'}
  ];

  let list1 = useListData({
    initialItems: [
      {id: '1', type: 'file', name: 'Adobe Photoshop', date: '6/7/2020'},
      {id: '2', type: 'file', name: 'Adobe XD', date: '4/7/2021'},
      {id: '3', type: 'folder', name: 'Documents', date: '4/9/2021'},
      {id: '4', type: 'file', name: 'Adobe InDesign', date: '11/20/2010'},
      {id: '5', type: 'folder', name: 'Utilities', date: '9/18/2016'},
      {id: '6', type: 'file', name: 'Adobe AfterEffects', date: '1/18/2016'}
    ]
  });

  let list2 = useListData({
    initialItems: [
      {id: '7', type: 'folder', name: 'Pictures', date: '6/7/2020'},
      {id: '8', type: 'file', name: 'Adobe Fresco', date: '4/2/2020'},
      {id: '9', type: 'folder', name: 'Apps', date: '2/3/2020'},
      {id: '10', type: 'file', name: 'Adobe Illustrator', date: '6/5/2020'},
      {id: '11', type: 'file', name: 'Adobe Lightroom', date: '3/7/2020'},
      {id: '12', type: 'file', name: 'Adobe Dreamweaver', date: '3/9/2020'}
    ]
  });


  return (
    <Flex wrap gap="size-300">
      <BidirectionalDnDTableView list={list1} columns={columns} aria-label="First TableView in drag between table example" />
      <BidirectionalDnDTableView list={list2} columns={columns} aria-label="Second TableView in drag between table example" />
    </Flex>
  );
}
```
</details>

```tsx example
<DragBetweenTablesExample />
```

### Overriding default drop operation

The example below demonstrates how to use `getDropOperation` to specify the default drop operation of the droppable table.

<details>
  <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>

```tsx example export=true render=false
function DraggableTableViewDefaultCopy(props: DndTableViewProps) {
  let {list, columns} = props;
  let {dragAndDropHooks} = useDragAndDrop({
    getItems: (keys) => [...keys].map(key => {
      let item = list.getItem(key);
      // Setup the drag types and associated info for each dragged item.
      return {
        'custom-app-type-copy-default': JSON.stringify(item),
        'text/plain': item.name
      };
    }),
    onDragEnd: (e) => {
      let {
        dropOperation,
        keys
      } = e;

      if (dropOperation === 'move') {
        list.remove(...keys);
      }
    }
  });

  return (
    <TableView
      aria-label="Draggable TableView in default copy operation example"
      selectionMode="multiple"
      width="size-5000"
      height="size-2400"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DroppableTableViewDefaultCopy(props: DndTableViewProps) {
  let {list, columns} = props;
  let {dragAndDropHooks} = useDragAndDrop({
    acceptedDragTypes: ['custom-app-type-copy-default'],
    /*- begin highlight -*/
    getDropOperation: () => 'copy',
    /*- end highlight -*/
    onInsert: async (e) => {
      let {
        items,
        target
      } = e;

      /*- begin highlight -*/
      // Create random id to allow for multiple copies of the same item
      let processedItems = await Promise.all(
        items.map(async (item) => (
          {...JSON.parse(await (item as TextDropItem).getText('custom-app-type-copy-default')), id: Math.random().toString(36).slice(2)}
        ))
      );
      /*- end highlight -*/
      if (target.dropPosition === 'before') {
        list.insertBefore(target.key, ...processedItems);
      } else if (target.dropPosition === 'after') {
        list.insertAfter(target.key, ...processedItems);
      }
    },
    onRootDrop: async (e) => {
      let {
        items
      } = e;

      /*- begin highlight -*/
      // Create random id to allow for multiple copies of the same item
      let processedItems = await Promise.all(
        items.map(async (item) => (
          {...JSON.parse(await (item as TextDropItem).getText('custom-app-type-copy-default')), id: Math.random().toString(36).slice(2)}
        ))
      );
      /*- end highlight -*/
      list.append(...processedItems);
    }
  });


  return (
    <TableView
      aria-label="Droppable TableView in default copy operation example"
      width="size-5000"
      height="size-2400"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}

function DragIntoTablesDefaultCopy() {
  let columns = [
    {name: 'Name', id: 'name'},
    {name: 'Type', id: 'type'},
    {name: 'Date Modified', id: 'date'}
  ];

  let sourceList = useListData({
    initialItems: [
      {id: '1', type: 'file', name: 'Adobe Photoshop', date: '6/7/2020'},
      {id: '2', type: 'file', name: 'Adobe XD', date: '4/7/2021'},
      {id: '3', type: 'file', name: 'Adobe InDesign', date: '11/20/2010'},
      {id: '4', type: 'file', name: 'Adobe AfterEffects', date: '1/18/2016'}
    ]
  });

  let targetList = useListData({
    initialItems: [
      {id: '5', type: 'file', name: 'Adobe Dreamweaver', date: '6/7/2020'},
      {id: '6', type: 'file', name: 'Adobe Fresco', date: '2/17/2020'},
      {id: '7', type: 'file', name: 'Adobe Connect', date: '1/23/2020'},
      {id: '8', type: 'file', name: 'Adobe Lightroom', date: '12/17/2020'}
    ]
  });


  return (
    <Flex wrap gap="size-300">
      <DraggableTableViewDefaultCopy list={sourceList} columns={columns} />
      <DroppableTableViewDefaultCopy list={targetList} columns={columns} />
    </Flex>
  );
}
```
</details>

```tsx example
<DragIntoTablesDefaultCopy />
```

### Custom drag previews

By default, the drag preview for a dragged row will include the `textValue` provided to that row.
If no `textValue` was provided, it will default to the contents of the columns that are row headers, as specified by the `isRowHeader` prop on columns.
If no row header columns were specified, the first column will be used, as mentioned in the [Accessibility](#accessibility) section.

Use the `renderPreview` prop to provide a custom drag preview. `keys` and `draggedKey` are passed to this function, where `keys` includes all the keys of the items being dragged, and `draggedKey` is the key of the item the user initiated the drag from.

<details>
  <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary>

```tsx example export=true render=false
import {View} from '@react-spectrum/view';

function CustomDragPreviewExample() {
  let columns = [
    {name: 'Name', id: 'name'},
    {name: 'Type', id: 'type'},
    {name: 'Date Modified', id: 'date'}
  ];

  let list = useListData({
    initialItems: [
      {id: '1', type: 'file', name: 'Adobe Photoshop', date: '6/3/2020'},
      {id: '2', type: 'file', name: 'Adobe XD', date: '2/2/2020'},
      {id: '3', type: 'folder', name: 'Documents', date: '5/1/2020', childNodes: []},
      {id: '4', type: 'file', name: 'Adobe InDesign', date: '5/9/2020'},
      {id: '5', type: 'folder', name: 'Utilities', date: '12/7/2020', childNodes: []},
      {id: '6', type: 'file', name: 'Adobe AfterEffects', date: '6/1/2020'}
    ]
  });
  let {dragAndDropHooks} = useDragAndDrop({
    getItems: (keys) => [...keys].map(key => {
      let item = list.getItem(key);
      return {
        'custom-app-type': JSON.stringify(item),
        'text/plain': item.name
      };
    }),
    /*- begin highlight -*/
    renderPreview: (keys, draggedKey) => (
      <View backgroundColor="gray-50" padding="size-100" borderRadius="medium" borderWidth="thin" borderColor="blue-500">
        <strong>Custom Preview</strong>
        <div>Keys: [{[...keys].join(', ')}]</div>
        <div>Dragged: {draggedKey}</div>
      </View>
    )
    /*- end highlight -*/
  });
  return (
    <TableView
      aria-label="Draggable TableView example with custom drag preview"
      selectionMode="multiple"
      height="size-3600"
      dragAndDropHooks={dragAndDropHooks}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            key={column.id}
            align={column.id === 'date' ? 'end' : 'start'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={list.items}>
        {item => (
          <Row>
            {columnKey => <Cell>{item[columnKey]}</Cell>}
          </Row>
        )}
      </TableBody>
    </TableView>
  );
}
```

</details>

```tsx example
<CustomDragPreviewExample />
```

## Props

### TableView props

<PropTable component={docs.exports.TableView} links={docs.links} style={{marginBottom: '40px'}} />

### TableHeader props

<PropTable component={{
  props: {
    properties: Object.fromEntries(Object.entries(tableTypes.exports.TableHeaderProps.properties))
  }
}} links={tableTypes.links} style={{marginBottom: '40px'}} />

### Column props

<PropTable component={{
  props: {
    properties: Object.fromEntries(Object.entries(tableTypes.exports.SpectrumColumnProps.properties))
  }
}} links={tableTypes.links} style={{marginBottom: '40px'}} />

### TableBody props

<PropTable component={{
  props: {
    properties: Object.fromEntries(Object.entries(tableTypes.exports.TableBodyProps.properties))
  }
}} links={tableTypes.links} style={{marginBottom: '40px'}} />

### Row props

<PropTable component={{
  props: {
    properties: Object.fromEntries(Object.entries(tableTypes.exports.RowProps.properties))
  }
}} links={tableTypes.links} style={{marginBottom: '40px'}} />

### Cell props

<PropTable component={{
  props: {
    properties: Object.fromEntries(Object.entries(tableTypes.exports.CellProps.properties))
  }
}} links={tableTypes.links} />

## Visual options
### Column alignment
[View guidelines](https://spectrum.adobe.com/page/table/#Usage-guidelines)

```tsx example
<TableView aria-label="Example table for column alignment">
  <TableHeader>
    <Column align="start">Name</Column>
    <Column align="center">Type</Column>
    <Column align="end">Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
```

### Column dividers
[View guidelines](https://spectrum.adobe.com/page/table/#Column-dividers)

```tsx example
<TableView aria-label="Example table for column dividers">
  <TableHeader>
    <Column align="start" showDivider>Name</Column>
    <Column showDivider>Type</Column>
    <Column align="end" showDivider>Size</Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>2021406_Proposal</Cell>
      <Cell>PDF</Cell>
      <Cell>86 KB</Cell>
    </Row>
    <Row>
      <Cell>Budget Template</Cell>
      <Cell>XLS</Cell>
      <Cell>120 KB</Cell>
    </Row>
    <Row>
      <Cell>Onboarding</Cell>
      <Cell>PPT</Cell>
      <Cell>472 KB</Cell>
    </Row>
    <Row>
      <Cell>Welcome</Cell>
      <Cell>TXT</Cell>
      <Cell>24 KB</Cell>
    </Row>
  </TableBody>
</TableView>
```

### Hide header
Individual column headers can be hidden by providing the `hideHeader` prop to the Column. A tooltip is rendered when the column header is focused
to compensate for the lack of a visual title. Note that the `hideHeader` prop is specifically intended for columns that contain ActionButtons instead
of text content.

```tsx example export=true
function TableExample(props) {
  let columns = [
    {name: 'First Name', key: 'firstName'},
    {name: 'Last Name', key: 'lastName'},
    {name: 'Add Info', key: 'addInfo'},
    {name: 'Age', key: 'age'}
  ];

  let rows = [
    {id: '1', firstName: 'John', lastName: 'Doe', age: '45'},
    {id: '2', firstName: 'Jane', lastName: 'Doe', age: '37'},
    {id: '3', firstName: 'Joe', lastName: 'Schmoe', age: '67'},
    {id: '4', firstName: 'Joe', lastName: 'Bloggs', age: '12'},
    {id: '5', firstName: 'Taylor', lastName: 'Rodriguez Lloyd-Atkinson', age: '83'}
  ];

  return (
    <TableView aria-label="Example table with hidden headers" maxWidth="size-6000" {...props}>
      <TableHeader columns={columns}>
        {column => (
          <Column
            hideHeader={column.key === 'addInfo'}
            align={column.key === 'age' ? 'end' : 'start'}
            showDivider={column.key === 'addInfo'}>
            {column.name}
          </Column>
        )}
      </TableHeader>
      <TableBody items={rows}>
        {item =>
          (<Row key={item.id}>
            {key =>
              key === 'addInfo'
              ? <Cell><ActionButton aria-label="Add Info" isQuiet><Add /></ActionButton></Cell>
              : <Cell>{item[key]}</Cell>
            }
          </Row>)
        }
      </TableBody>
    </TableView>
  );
}
```

### Quiet
[View guidelines](https://spectrum.adobe.com/page/table/#Standard-or-quiet)

```tsx example
// Using same setup as hide header example
<TableExample isQuiet />
```

### Density
The amount of vertical padding that each row contains can be modified by providing the `density` prop.

```tsx example
// Using same setup as hide header example
<Flex direction="column" gap="size-300">
  <TableExample density="compact" />
  <TableExample density="spacious" />
</Flex>
```

### Overflow mode
By default, text content that overflows its table cell will be truncated. You can have it wrap instead by passing `overflowMode="wrap"`
to the TableView.

```tsx example
// Using same setup as hide header example
<TableExample overflowMode="wrap" />
```

### Empty state
Use the `renderEmptyState` prop to customize what the TableView will display if there are no rows provided.

```tsx example
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';

function renderEmptyState() {
  return (
    <IllustratedMessage>
      <NotFound />
      <Heading>No results</Heading>
      <Content>No results found</Content>
    </IllustratedMessage>
  );
}

<TableView aria-label="Example table for empty state" height="size-3000" renderEmptyState={renderEmptyState}>
  <TableHeader>
    <Column>Name</Column>
    <Column>Type</Column>
    <Column>Size</Column>
  </TableHeader>
  <TableBody>
    {[]}
  </TableBody>
</TableView>
```

### Nested columns

TableView supports nesting columns, allowing you to create column groups, or "tiered" column headers. Data for the leaf columns appears in each row of the table body.

```tsx example
<TableView aria-label="Example table for nested columns">
  <TableHeader>
    <Column title="Name">
      <Column isRowHeader>First Name</Column>
      <Column isRowHeader>Last Name</Column>
    </Column>
    <Column title="Information">
      <Column>Age</Column>
      <Column>Birthday</Column>
    </Column>
  </TableHeader>
  <TableBody>
    <Row>
      <Cell>Sam</Cell>
      <Cell>Smith</Cell>
      <Cell>36</Cell>
      <Cell>May 3</Cell>
    </Row>
    <Row>
      <Cell>Julia</Cell>
      <Cell>Jones</Cell>
      <Cell>24</Cell>
      <Cell>February 10</Cell>
    </Row>
    <Row>
      <Cell>Peter</Cell>
      <Cell>Parker</Cell>
      <Cell>28</Cell>
      <Cell>September 7</Cell>
    </Row>
    <Row>
      <Cell>Bruce</Cell>
      <Cell>Wayne</Cell>
      <Cell>32</Cell>
      <Cell>December 18</Cell>
    </Row>
  </TableBody>
</TableView>
```

Nested columns can also be defined dynamically using the function syntax and the `childColumns` prop.
The following example is the same as the example above, but defined dynamically.

```tsx example
interface ColumnDefinition {
  name: string,
  key: string,
  children?: ColumnDefinition[],
  isRowHeader?: boolean
}

let columns: ColumnDefinition[] = [
  {name: 'Name', key: 'name', children: [
    {name: 'First Name', key: 'first', isRowHeader: true},
    {name: 'Last Name', key: 'last', isRowHeader: true}
  ]},
  {name: 'Information', key: 'info', children: [
    {name: 'Age', key: 'age'},
    {name: 'Birthday', key: 'birthday'}
  ]}
];

let rows = [
  {id: 1, first: 'Sam', last: 'Smith', age: 36, birthday: 'May 3'},
  {id: 2, first: 'Julia', last: 'Jones', age: 24, birthday: 'February 10'},
  {id: 3, first: 'Peter', last: 'Parker', age: 28, birthday: 'September 7'},
  {id: 4, first: 'Bruce', last: 'Wayne', age: 32, birthday: 'December 18'}
];

<TableView aria-label="Example table for nested columns">
  <TableHeader columns={columns}>
    {column => (
      <Column isRowHeader={column.isRowHeader} childColumns={column.children}>
        {column.name}
      </Column>
    )}
  </TableHeader>
  <TableBody items={rows}>
    {item => (
      <Row>
        {columnKey => <Cell>{item[columnKey]}</Cell>}
      </Row>
    )}
  </TableBody>
</TableView>
```

## Testing

The TableView features automatic virtualization and may need specific mocks in a test environment to enable said virtualization properly.
It also features long press interactions on its rows depending on the row actions provided and if user is interacting with the list on
a touch device. Please see the following sections in the testing docs for more information on how to handle these
behaviors in your test suite.

[Timers](./testing.html#timers)

[Desktop vs Mobile](./testing.html#desktop-vs-mobile)

[Virtualized Components](./testing.html#virtualized-components)

[Long press](./testing.html#simulating-user-long-press)

Please also refer to [React Spectrum's test suite](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/table/test/Table.test.js) if you find that the above
isn't sufficient when resolving issues in your own test cases.

### Test utils <VersionBadge version="beta" style={{marginLeft: 4, verticalAlign: 'bottom'}} />

`@react-spectrum/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See [here](./testing.html#react-spectrum-test-utils) for more information on how to setup these utilities
in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite.

```ts
// TableView.test.ts
import {render, within} from '@testing-library/react';
import {theme} from '@react-spectrum/theme-default';
import {User} from '@react-spectrum/test-utils';

let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/TableView.html#testing

it('TableView can toggle row selection', async function () {
  // Render your test component/app and initialize the table tester
  let {getByTestId} = render(
    <Provider theme={defaultTheme}>
      <TableView data-testid="test-table" selectionMode="multiple">
      ...
      </TableView>
    </Provider>
  );
  let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')});
  expect(tableTester.selectedRows).toHaveLength(0);

  await tableTester.toggleSelectAll();
  expect(tableTester.selectedRows).toHaveLength(10);

  await tableTester.toggleRowSelection({row: 2});
  expect(tableTester.selectedRows).toHaveLength(9);
  let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
  expect(checkbox).not.toBeChecked();

  await tableTester.toggleSelectAll();
  expect(tableTester.selectedRows).toHaveLength(10);
  expect(checkbox).toBeChecked();

  await tableTester.toggleSelectAll();
  expect(tableTester.selectedRows).toHaveLength(0);
});
```

<ClassAPI links={tableUtil.links} class={tableUtil.exports.TableTester} />
